﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
using System.Drawing.Imaging;
using Valentia.Kinection.DataTypes;

namespace Valentia.Kinection.Service
{
    #region MESSAGE TYPE ENUM
    
    /// <summary>
    /// Message type enum.
    /// </summary>
    internal enum MessageType
    {
        /// <summary>
        /// Color image.
        /// </summary>
        ColorImage = 1,

        /// <summary>
        /// Depth image.
        /// </summary>
        DepthImage = 2,

        /// <summary>
        /// Skeleton data.
        /// </summary>
        SkeletonData = 3,

        /// <summary>
        /// Authentication request.
        /// </summary>
        AuthRequest = 4,

        /// <summary>
        /// Authentication reply.
        /// </summary>
        AuthReply = 5
    };

    #endregion

    #region UTILITY CLASS

    /// <summary>
    /// Utility class.
    /// </summary>
    internal class Utilties
    {
        #region METHODS
        /// <summary>
        /// Gets the string for resoloution.
        /// </summary>
        /// <param name="imageFormat">Resoloution enum value.</param>
        /// <returns>Resoloution string.</returns>
        internal static String GetResoloutionString(KinectColorImageFormat imageFormat)
        {
            switch (imageFormat)
            {
                case KinectColorImageFormat.RawYuvResolution640x480Fps15: return "640,480";
                case KinectColorImageFormat.RgbResolution1280x960Fps12: return "1280,960";
                case KinectColorImageFormat.RgbResolution640x480Fps30: return "640,480";
                case KinectColorImageFormat.YuvResolution640x480Fps15: return "640,480";
                default: return "640,480";
            }
        }

        /// <summary>
        /// Gets the string for resoloution.
        /// </summary>
        /// <param name="imageFormat">Resoloution enum value.</param>
        /// <returns>Resoloution string.</returns>
        internal static String GetResoloutionString(KinectDepthImageFormat imageFormat)
        {
            switch (imageFormat)
            {
                case KinectDepthImageFormat.Resolution320x240Fps30: return "320,240";
                case KinectDepthImageFormat.Resolution640x480Fps30: return "640,480";
                case KinectDepthImageFormat.Resolution80x60Fps30: return "80,60";
                default: return "80,60";
            }
        }

        /// <summary>
        /// Gets the image as string.
        /// </summary>
        /// <param name="image">Image.</param>
        /// <returns>Image as string.</returns>
        internal static String GetImageString(Image image)
        {
            ImageFormat imageFormat = ImageFormat.Png;
            
            // Get the Image extension/type
            String imageType = imageFormat.ToString().ToLower().Trim();

            using (MemoryStream ms = new MemoryStream())
            {
                // saving image to memory stream
                image.Save(ms, imageFormat);
                ms.Flush();

                String imageBase64 = Convert.ToBase64String(ms.ToArray());
                return String.Format("data:image/{0};base64,{1}", imageType, imageBase64); //"data:image/png;base64,<image data's base 64 string here>"
            }
        }

        /// <summary>
        /// Corrupts the string by replacing the character order in it.
        /// <para>This method will reorder the each 4 characters group as 2031. Group index count starts from 0.</para>
        /// <para>If any group has characters less than 4 then spaces will be added to make its length 4.</para>
        /// </summary>
        /// <param name="stringValue">String to corrupt.</param>
        /// <returns>Corrupted string.</returns>
        internal static String CorruptString(String stringValue)
        {
            return stringValue;
        }

        /// <summary>
        /// Reorder the corrupted string by placing each character to its original order.
        /// </summary>
        /// <param name="corruptedString">Corrupted string.</param>
        /// <returns>Original string.</returns>
        internal static String ReorderString(String corruptedString)
        {
            int remChars = 4 - corruptedString.Length % 4;
            if (remChars > 0 && remChars < 4) { corruptedString += new String(' ', remChars); }

            String orderedString = String.Empty;
            for (int index = 0; index < corruptedString.Length; index += 4)
            {
                orderedString +=
                    (
                    corruptedString[index + 1].ToString() +
                    corruptedString[index + 3].ToString() +
                    corruptedString[index + 0].ToString() + corruptedString[index + 2].ToString()
                    );
            }
            return orderedString.Trim();
        }
        #endregion
    }
    #endregion

    #region MESSAGE BASE
    /// <summary>
    /// Message base class.
    /// </summary>
    internal class MessageBase
    {
        #region DATA MEMBERS
        /// <summary>
        /// Line seprator for each content of message.
        /// </summary>
        protected static readonly String LF = "\n";
        #endregion

        #region PROPERTIES
        /// <summary>
        /// Gets the message type.
        /// </summary>
        public MessageType MessageType { get; private set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="messageType">Message type.</param>
        protected MessageBase(MessageType messageType) { this.MessageType = messageType; }
        #endregion

        #region METHDOS
        /// <summary>
        /// Reads the string message and constructs the appropriate message instance.
        /// </summary>
        /// <param name="message">String message</param>
        /// <returns>Message</returns>
        internal static MessageBase DecodeString(String message)
        {
            int index = 0, messageTypeInt = 0;
            String[] contents = message.Split(MessageBase.LF.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            
            messageTypeInt = int.Parse(contents[index++].Trim());
            MessageType messageType = (MessageType)(messageTypeInt);

            switch (messageType)
            {
                #region AUTH. REQUEST MESSAGE
                case MessageType.AuthRequest:
                    {
                        String login = Utilties.ReorderString(contents[index++]);
                        String password = Utilties.ReorderString(contents[index++]);

                        return new AuthRequest() { Login = login, Password = password };
                    }
                #endregion
            }

            throw new Exception(String.Format("Invalid message type received: {0}", messageType.ToString()));
        }

        /// <summary>
        /// Encodes the message as string.
        /// </summary>
        /// <returns>Message as string.</returns>
        internal String Encode()
        {
            StringBuilder sb = new StringBuilder(100);
            sb.AppendFormat("{0}{1}", ((int)this.MessageType), MessageBase.LF);

            this.WriteContents(sb);
            return sb.ToString();
        }

        /// <summary>
        /// When overidden in child class writes the contents as a  in the string builder object.
        /// </summary>
        /// <param name="sb">String builder.</param>
        protected virtual void WriteContents(StringBuilder sb) { }
        #endregion
    }
    #endregion

    #region COLOR IMAGE
    /// <summary>
    ///  Color Image.
    ///  <para>============</para>
    ///  <para>1&lt;LF&gt;</para>
    ///  <para>Kinect Device Name&lt;LF&gt;</para>
    ///  <para>Resoloution(e.g. 320,240)&lt;LF&gt;</para>
    ///  <para>...&lt;LF&gt;</para>
    ///  </summary>
    internal class ColorImage : MessageBase
    {
        #region PROPERTIES
        /// <summary>
        /// Gets or sets the kinect device name.
        /// </summary>
        internal String KinectDevice { get; set; }

        /// <summary>
        /// Gets or sets the color image format.
        /// </summary>
        internal KinectColorImageFormat ColorImageFormat { get; set; }

        /// <summary>
        /// Gets or sets the color image.
        /// </summary>
        internal Image Image { get; set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        internal ColorImage()
            : base(MessageType.ColorImage)
        {
            this.KinectDevice = String.Empty;
            this.ColorImageFormat = KinectColorImageFormat.YuvResolution640x480Fps15;
            this.Image = null;
        }
        #endregion

        #region METHDOS
        /// <summary>
        /// When overidden in child class writes the contents as a string in the string builder object.
        /// </summary>
        /// <param name="sb">String builder.</param>
        protected override void WriteContents(StringBuilder sb)
        {
            sb.AppendFormat("{0}{1}", this.KinectDevice, MessageBase.LF);
            sb.AppendFormat("{0}{1}", Utilties.GetResoloutionString(this.ColorImageFormat), MessageBase.LF);
            sb.AppendFormat("{0}{1}", Utilties.GetImageString(this.Image), MessageBase.LF);
        }
        #endregion
    }
    #endregion

    #region DEPTH IMAGE
    /// <summary>
    /// <para>Color Image.</para>
    /// <para>============</para>
    /// <para>2&lt;LF&gt;</para>
    /// <para>Kinect Device Name&lt;LF&gt;</para>
    /// <para>Resoloution(e.g. 320,240)&lt;LF&gt;</para>
    /// <para>...&lt;LF&gt;</para>
    /// <para>BytesPerPixel&lt;LF&gt;</para>
    /// <para>Width, Height&lt;LF&gt;</para>
    /// <para>1,2,3,4,5,6 ... &lt;LF&gt;</para>
    ///  </summary>
    internal class DepthImage : MessageBase
    {
        #region PROPERTIES
        /// <summary>
        /// Gets or sets the kinect device name.
        /// </summary>
        internal String KinectDevice { get; set; }

        /// <summary>
        /// Gets or sets the depth image format.
        /// </summary>
        internal KinectDepthImageFormat DepthImageFormat { get; set; }

        /// <summary>
        /// Gets or sets the depth image.
        /// </summary>
        internal Image Image { get; set; }

        /// <summary>
        /// Gets or sets the bytes per pixel.
        /// </summary>
        internal int BytesPerPixel { get; set; }

        /// <summary>
        /// Gets or sets the widht of image.
        /// </summary>
        internal int Width { get; set; }

        /// <summary>
        /// Gets or sets the height of image.
        /// </summary>
        internal int Height { get; set; }

        /// <summary>
        /// Gets or sets the image bits data.
        /// </summary>
        internal short[] ImageBitData { get; set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        internal DepthImage()
            : base(MessageType.DepthImage)
        {
            this.KinectDevice = String.Empty;
            this.DepthImageFormat = KinectDepthImageFormat.Resolution320x240Fps30;
            this.Image = null;
            this.BytesPerPixel = 0;
            this.Width = 0;
            this.Height = 0;
            this.ImageBitData = new short[0];
        }
        #endregion

        #region METHDOS
        /// <summary>
        /// When overidden in child class writes the contents as a string in the string builder object.
        /// </summary>
        /// <param name="sb">String builder.</param>
        protected override void WriteContents(StringBuilder sb)
        {
            sb.AppendFormat("{0}{1}", this.KinectDevice, MessageBase.LF);
            sb.AppendFormat("{0}{1}", Utilties.GetResoloutionString(this.DepthImageFormat), MessageBase.LF);
            sb.AppendFormat("{0}{1}", Utilties.GetImageString(this.Image), MessageBase.LF);

            sb.AppendFormat("{0}{1}", this.BytesPerPixel, MessageBase.LF);
            sb.AppendFormat("{0},{1}{2}", this.Width, this.Height, MessageBase.LF);

            if (this.ImageBitData != null && this.ImageBitData.Length > 0)
            {
                foreach (byte bit in this.ImageBitData)
                {
                    sb.AppendFormat("{0},", bit);
                }

                //remove the last comma (,) sign
                sb.Remove(sb.Length - 1, 1); 
            }
            sb.Append(MessageBase.LF);
        }
        #endregion
    }
    #endregion

    #region SKELETON DATA
    /// <summary>
    /// <para>Skeleton Data</para>
    /// <para>=============</para>
    /// <para>3&lt;LF&gt;</para>
    /// <para>Kinect Device Name&lt;LF&gt;</para>
    /// <para>Resoloution(e.g. 320,240)&lt;LF&gt;</para>
    /// <para>NoOfSkeletons&lt;LF&gt;</para>
    /// <para></para>
    /// <para>TrackingID$</para>
    /// <para>UserID$</para>
    /// <para>Qulatiy$</para>
    /// <para>NoOfJoints$</para>
    /// <para>JointID, w, x, y, z, rx, ry$</para>
    /// <para>JointID, w, x, y, z, rx, ry$</para>
    /// <para>JointID, w, x, y, z, rx, ry&lt;LF&gt;</para>
    /// <para></para>
    /// <para>TrackingID$</para>
    /// <para>UserID$</para>
    /// <para>NoOfJoints$</para>
    /// <para>JointID, w, x, y, z, rx, ry$</para>
    /// <para>JointID, w, x, y, z, rx, ry$</para>
    /// <para>JointID, w, x, y, z, rx, ry&lt;LF&gt;</para>
    /// </summary>
    internal class SkeletonData : MessageBase
    {
        #region DATA MEMBERS
        private static readonly String InnerSeprator = "$";
        #endregion

        #region PROPERTIES
        /// <summary>
        /// Gets or sets the skeleton frame.
        /// </summary>
        internal SkeletonFrameReceivedArgs SkeletonFrame { get; set; }

        /// <summary>
        /// Gets the array of joints which are to be sent.
        /// </summary>
        internal SkeletonJointID[] JointsToSend { get; set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="jointsToSend">Joint to send.</param>
        internal SkeletonData(params SkeletonJointID[] jointsToSend) : base(MessageType.SkeletonData) { this.JointsToSend = jointsToSend; this.SkeletonFrame = null; }
        #endregion

        #region METHDOS
        /// <summary>
        /// When overidden in child class writes the contents as a string in the string builder object.
        /// </summary>
        /// <param name="sb">String builder.</param>
        protected override void WriteContents(StringBuilder sb)
        {
            sb.AppendFormat("{0}{1}", this.SkeletonFrame.KinectDevice, MessageBase.LF);
            sb.AppendFormat("{0}{1}", Utilties.GetResoloutionString(this.SkeletonFrame.ColorImageResoloution), MessageBase.LF);
            sb.AppendFormat("{0}{1}", this.SkeletonFrame.Skeletons.Length, MessageBase.LF);

            if (this.SkeletonFrame.Skeletons.Length <= 0) { sb.Append(MessageBase.LF); return; }

            foreach (SkeletonJointCollection skeleton in this.SkeletonFrame.Skeletons)
            {
                int userIndex = 0;
                if (skeleton.Count > 0) { userIndex = skeleton[0].JointDepth.PlayerIndex; }

                sb.AppendFormat("{0}{1}", skeleton.TrackingID, SkeletonData.InnerSeprator);
                sb.AppendFormat("{0}{1}", userIndex, SkeletonData.InnerSeprator); //skeleton.UserIndex
                sb.AppendFormat("{0}{1}", skeleton.ClippedEdges, SkeletonData.InnerSeprator);

                if (this.JointsToSend == null || this.JointsToSend.Length <= 0) { sb.AppendFormat("0{0}{1}", SkeletonData.InnerSeprator, MessageBase.LF); return; }

                int countExistCount = 0;
                foreach (SkeletonJointID jointID in this.JointsToSend) { if (jointID == SkeletonJointID.Count) { countExistCount++; } }

                sb.AppendFormat("{0}{1}", (this.JointsToSend.Length - countExistCount), SkeletonData.InnerSeprator);

                SkeletonJointInfo joint = null;
                foreach (SkeletonJointID jointID in this.JointsToSend)
                {
                    if (jointID == SkeletonJointID.Count) continue;

                    joint = skeleton[jointID];
                    sb.AppendFormat(
                        "{0},{1},{2},{3},{4},{5},{6}{7}", (int)(joint.JointID), joint.Vector.W, joint.Vector.X, joint.Vector.Y, joint.Vector.Z,
                        joint.ColorX, joint.ColorY, SkeletonData.InnerSeprator);
                }

                sb.Remove(sb.Length - 1, 1); //remove the last dollar ($) sign
                sb.Append(MessageBase.LF);
            }
        }
        #endregion
    }
    #endregion

    #region AUTH. REQUEST
    /// <summary>
    /// <para>Auth. Request from Web client.</para>
    /// <para>==============================</para>
    /// <para>4&lt;LF&gt;</para>
    /// <para>Login&lt;LF&gt;</para>
    /// <para>Pwd&lt;LF&gt;</para>
    /// </summary>
    internal class AuthRequest : MessageBase
    {
        #region PROPERTIES
        /// <summary>
        /// Gets or sets the login of web socket client.
        /// </summary>
        internal String Login { get; set; }

        /// <summary>
        /// Gets or sets the password of web socket client.
        /// </summary>
        internal String Password { get; set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        internal AuthRequest()
            : base(MessageType.AuthRequest)
        {
            this.Login = String.Empty;
            this.Password = String.Empty;
        }
        #endregion

        #region METHDOS
        /// <summary>
        /// When overidden in child class writes the contents as a string in the string builder object.
        /// </summary>
        /// <param name="sb">String builder.</param>
        protected override void WriteContents(StringBuilder sb)
        {
            sb.AppendFormat("{0}{1}", Utilties.CorruptString(this.Login), MessageBase.LF);
            sb.AppendFormat("{0}{1}", Utilties.CorruptString(this.Password), MessageBase.LF);
        }
        #endregion
    }
    #endregion

    #region AUTH. REPLY
    /// <summary>
    /// <para>Auth. Reply from web socket server.</para>
    /// <para>==================================</para>
    /// <para>5&lt;LF&gt;</para>
    /// <para>True&lt;LF&gt;</para>
    /// <para>Reason&lt;LF&gt;</para>
    /// </summary>
    internal class AuthReply : MessageBase
    {
        #region PROPERTIES
        /// <summary>
        /// Gets or sets the status of web socket client. True if authenticated else false.
        /// </summary>
        internal bool Status { get; set; }

        /// <summary>
        /// Gets or sets the reason of disconnection.
        /// </summary>
        internal String Reason { get; set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        internal AuthReply()
            : base(MessageType.AuthReply)
        {
            this.Status = true;
            this.Reason = String.Empty;
        }
        #endregion

        #region METHDOS
        /// <summary>
        /// When overidden in child class writes the contents as a string in the string builder object.
        /// </summary>
        /// <param name="sb">String builder.</param>
        protected override void WriteContents(StringBuilder sb)
        {
            sb.AppendFormat("{0}{1}", this.Status, MessageBase.LF);
            sb.AppendFormat("{0}{1}", this.Reason, MessageBase.LF);
        }
        #endregion
    }
    #endregion
}
